﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
#if !WINDOWS_PHONE


#endif

namespace DARE
{
    [AttrTypeDependentSerializable(typeof(DARE.Save.DataStructures.CCameraData))]
    public class CCamera : IUpdateable
    {

    #region fields

        // view param
        private float m_fovy = MathHelper.PiOver4;
        private float m_aspectRatio = CDare.Instance.GraphicsDevice.Viewport.AspectRatio;
        private float m_nearPlane = 0.1f;
        private float m_farPlane = 500.0f;

        // update bool, avoid useless calcul
        protected bool m_needUpdateProjection = true;
        protected bool m_needUpdateFrustrum = true;
        protected bool m_needUpdateView = true;

        // camera matrix and frustrum
        protected Matrix m_viewMatrix;
        protected Matrix m_projectionMatrix;
        protected BoundingFrustum m_frustrum;

        // transform
        protected CRender3D m_render;
        protected CNode m_node;
        private CNode m_saveNode;

        // properties
        protected bool m_active = false;
        protected string m_name;
        private bool m_updatable = true; // useless but inherits from IUpdatable...
        private bool m_keepTransform = true;
        private bool m_attachedToNode = false;
        private bool m_manualAspectRatio = false;
        private float m_criticalValuesOffset = 0.01f; // used when approaching a critical value, like the aspect ratio limit

    #endregion

    #region properties

        public CViewPort ViewPort { get; set; }

        /// <summary>
        /// Get or set if the aspect ratio can be changed manualy.
        /// </summary>
        public bool ManualAspectRatio
        {
            get { return m_manualAspectRatio; }
            set
            {
                if (m_manualAspectRatio != value)
                {
                    m_manualAspectRatio = value;
                    m_needUpdateProjection = true;
                }
            }
        }

        /// <summary>
        /// Activate the camera will render the scene throught it.
        /// Get if the camera is active.
        /// Internal set the camera active.
        /// </summary>
        public bool IsActive
        {
            get { return m_active; }
            internal set { m_active = value; }
        }

        /// <summary>
        /// Because it inherits from IUpdatable...
        /// </summary>
        public bool Updatable
        {
            get { return m_updatable; }
            set { m_updatable = value; }
        }

        /// <summary>
        /// When detach from a node, set if the camera will keep his transformations.
        /// </summary>
        public bool KeepTransform
        {
            get { return m_keepTransform; }
            set { m_keepTransform = value; }
        }

        /// <summary>
        /// Return the if camera is attached to a node.
        /// </summary>
        public bool AttachedToNode
        {
            get { return m_attachedToNode; }
        }

        /// <summary>
        /// Get the projection matrix.
        /// Set the projection, WARNING it was for tests, do not use.
        /// </summary>
        public Matrix Projection
        {
            get
            {
                if (m_needUpdateProjection)
                    UpdateProjection();
                return m_projectionMatrix;
            }
            // Tests Julio
            set
            {
                m_projectionMatrix = value;
                m_needUpdateProjection = true;
            }
        }

        /// <summary>
        /// Get the view matrix.
        /// </summary>
        public Matrix View
        {
            get 
            {
                if (m_needUpdateView)
                    UpdateView();
                return m_viewMatrix;
            }
        }

        /// <summary>
        /// Get the bounding frustrum of the camera.
        /// </summary>
        public BoundingFrustum Frustrum
        {
            get
            {
                if (m_needUpdateFrustrum)
                    UpdateFrustrum();
                return m_frustrum;
            }
        }

        /// <summary>
        /// The near plane is the distance from wich the scene is rendered.
        /// Get or set the near plane of the camera.
        /// </summary>
        public float NearPlane
        {
            get { return m_nearPlane; }
            set
            {
                if (value >= 0)
                {
                    m_nearPlane = value;
                    UpdateAspectRatio();
                }
            }
        }

        /// <summary>
        /// The far plane is the maximum rendering distance.
        /// Get or set the far plane of the camera.
        /// </summary>
        public float FarPlane
        {
            get { return m_farPlane; }
            set
            {
                if (value > m_nearPlane)
                {
                    m_farPlane = value;
                    UpdateAspectRatio();
                }
            }
        }

        /// <summary>
        /// Get the aspect ratio.
        /// Set the aspect ratio If ManualAscpectRatio is true.
        /// </summary>
        public float AspectRatio
        {
            get
            {
                return m_manualAspectRatio ? 
                    m_aspectRatio : 
                    CDare.Instance.Game.GraphicsDevice.Viewport.AspectRatio;
            }
            set
            {
                if (m_aspectRatio != value)
                {
                    m_aspectRatio = value;
                    m_needUpdateProjection = true;
                }
            }
        }

        /// <summary>
        /// Get the field of view.
        /// Set the field of view, minimum is 0.01, and maximum is 0.99 radians.
        /// </summary>
        public float FovY
        {
            get { return m_fovy; }
            set
            {
                if (m_fovy != value)
                {
                    m_fovy = value;
                    if (m_fovy < m_criticalValuesOffset)
                        m_fovy = m_criticalValuesOffset;
                    if (m_fovy > MathHelper.Pi - m_criticalValuesOffset)
                        m_fovy = MathHelper.Pi - m_criticalValuesOffset;
                    m_needUpdateProjection = true;
                }
            }
        }

        /// <summary>
        /// Get the camera's position.
        /// Set the camera's position if it's not attached to a node.
        /// </summary>
        public Vector3 Position
        {
            get { return m_node.Position; }
            set
            {
                if (!AttachedToNode)
                {
                    m_node.SetPosition(value, CNode.ESpace.WORLD);
                    m_needUpdateView = true;
                }
            }
        }

        /// <summary>
        /// Get the camera's orientation.
        /// Set the camera's orientation if it's not attached to a node.
        /// </summary>
        public Quaternion Orientation
        {
            get { return m_node.Orientation; }
            set
            {
                if (!AttachedToNode)
                {
                    m_node.SetOrientation(value, CNode.ESpace.WORLD);
                    m_needUpdateView = true;
                }
            }
        }

        /// <summary>
        /// Get the heading vector of the camera.
        /// This is the vector used to move forward or backward.
        /// </summary>
        public Vector3 Heading
        {
            get { return -m_node.GetLocalAxisZ(); }
        }

        /// <summary>
        /// Get the up vector of the camera.
        /// This is the vector used to move up or down.
        /// </summary>
        public Vector3 Up
        {
            get { return m_node.GetLocalAxisY(); }
        }

        /// <summary>
        /// Get the strafe vector of the camera.
        /// This is the vector used to strafe left or right.
        /// </summary>
        public Vector3 Strafe
        {
            get { return m_node.GetLocalAxisX(); }
        }

        /// <summary>
        /// Get or set the target's position of the camera.
        /// This is the actual camera position plus the heading vector.
        /// </summary>
        public Vector3 Target
        {
            get { return Position + Heading; }
            set { m_node.LookAt(value); }
        }

        /// <summary>
        /// Get the camera's name.
        /// </summary>
        public string Name
        {
            get { return m_name; }
        }

        /// <summary>
        /// Change the camera's node direclty only if you are sure of what you do.
        /// This is the node used by the camera for its transformation.
        /// If the camera is attached to another node it will move the attached node.
        /// Get the camera's node.
        /// </summary>
        public CNode Node
        {
            get
            {
                m_needUpdateView = true;
                return m_node; 
            }
        }

        public bool IsRendering { get; set; }

    #endregion

    #region ctor

        /// <summary>
        /// Create a camera, only used to serialize.
        /// </summary>
        public CCamera() { IsRendering = true; ViewPort = new CViewPort(); }

        /// <summary>
        /// Create a camera.
        /// </summary>
        /// <param name="name">Name of the camera.</param>
        public CCamera(string name)
        {
            IsRendering = true;
            ViewPort = new CViewPort();
            m_name = name;
            // create an entity because of the space transform demo (sprite 3d on the camera's nodes)
            m_node = new CNode(EType.DYNAMIC, true);
            m_frustrum = new BoundingFrustum(Matrix.Identity);
            SetLookAt(Position, Heading);
        }

    #endregion

    #region methods

        /// <summary>
        /// Update the view if the camera is attached to a node.
        /// Otherwise the camera is updated when accessing to the view matrix.
        /// </summary>
        /// <param name="gameTime"></param>
        public virtual void Update(GameTime gameTime)
        {
            if (m_attachedToNode)
                m_needUpdateView = true;
            m_node.Update(gameTime);
            m_node.Draw(gameTime);
        }

        /// <summary>
        /// Update the fov, aspect ratio, near plane and far plane.
        /// </summary>
        public void UpdateAspectRatio()
        {
            SetPerspectiveFov(m_fovy, m_aspectRatio, m_nearPlane, m_farPlane);
            UpdateProjection();
            UpdateFrustrum();
        }

        /// <summary>
        /// Set the fovy, the aspect ratio, the near plane and the far plane.
        /// </summary>
        /// <param name="fovy">Field of view.</param>
        /// <param name="aspectRatio">Aspect ratio.</param>
        /// <param name="nearPlane">Near plane.</param>
        /// <param name="farPlane">Far plane.</param>
        public void SetPerspectiveFov(float fovy, float aspectRatio, float nearPlane, float farPlane)
        {
            FovY = fovy;
            AspectRatio = aspectRatio;
            m_nearPlane = nearPlane;
            m_farPlane = farPlane;
            m_needUpdateProjection = true;
        }

        /// <summary>
        /// Set the camera to look at the target if its not attached to a node.
        /// Z axis is locked so the camera will never roll.
        /// </summary>
        /// <param name="cameraTarget">Target to look at.</param>
        public void LookAt(Vector3 cameraTarget)
        {
            if (m_attachedToNode)
                return;
            m_node.LookAt(cameraTarget);
            m_needUpdateView = true;
        }

        /// <summary>
        /// Same as LookAt but add the position of the camera.
        /// </summary>
        /// <param name="cameraPos">Position to set the camera.</param>
        /// <param name="cameraTarget">Target to look at.</param>
        public void SetLookAt(Vector3 cameraPos, Vector3 cameraTarget)
        {
            if (m_attachedToNode)
                return;
            m_node.SetPosition(cameraPos, CNode.ESpace.WORLD);
            m_node.LookAt(cameraTarget);
            m_needUpdateView = true;
        }

        /// <summary>
        /// Set the camera transform to a node's transform if its not attached to a node.
        /// </summary>
        /// <param name="node">Node.</param>
        public void SetToNode(CNode node)
        {
            if (m_attachedToNode)
                return;
            m_node.SetPosition(node.Position, CNode.ESpace.WORLD);
            m_node.SetOrientation(node.Orientation, CNode.ESpace.WORLD);
            m_needUpdateView = true;
        }

        /// <summary>
        /// Attach the camera to a node.
        /// All the transformations made direclty on the camera will be ignored if attached, 
        /// the only way to move it anyway is by the property "Node" of the camera, see "Node" 
        /// for more informations.
        /// The camera is now updating its transformations from the node it's attached to.
        /// </summary>
        /// <param name="node"></param>
        public void AttachToNode(CNode node)
        {
            if (m_node.Equals(node))
                return;
            if ((m_saveNode != null) && 
               m_saveNode.Equals(node))
            {
                DetachFromNode();
                return;
            }
            if (!m_attachedToNode)
                m_saveNode = m_node;
            m_node = node;
            m_attachedToNode = true;
            m_needUpdateView = true;
        }

        /// <summary>
        /// Detach the camera from a node.
        /// The cemera's position will be restored or kept according to the KeepTransform property.
        /// </summary>
        public void DetachFromNode()
        {
            if (m_saveNode == null)
                return;
            if (!m_keepTransform)
            {
                m_saveNode.SetPosition(m_node.Position, CNode.ESpace.WORLD);
                m_saveNode.SetOrientation(m_node.Orientation, CNode.ESpace.WORLD);
            }
            m_node = m_saveNode;
            m_attachedToNode = false;
            m_needUpdateView = true;
        }

        /// <summary>
        /// Update the projection matrix.
        /// </summary>
        protected virtual void UpdateProjection()
        {
            float aspectRatio = m_manualAspectRatio ? m_aspectRatio : CDare.Instance.Game.GraphicsDevice.Viewport.AspectRatio;

            m_projectionMatrix = Matrix.CreatePerspectiveFieldOfView(m_fovy, 
                aspectRatio, m_nearPlane, m_farPlane);
            m_needUpdateProjection = false;
            m_needUpdateFrustrum = true;
        }

        /// <summary>
        /// Update the view matrix.
        /// </summary>
        protected virtual void UpdateView()
        {
            m_viewMatrix = Matrix.CreateLookAt(Position, Target, Up);
            m_needUpdateView = false;
            m_needUpdateFrustrum = true;
        }

        /// <summary>
        /// Update the bounding frustrum.
        /// </summary>
        protected virtual void UpdateFrustrum()
        {
            m_frustrum.Matrix = m_viewMatrix * m_projectionMatrix;
            m_needUpdateFrustrum = false;
        }

    #endregion

    }
}
